package eu.sweetlygeek.scrobbler;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.UIManager.LookAndFeelInfo;

import net.roarsoftware.lastfm.Authenticator;
import net.roarsoftware.lastfm.Caller;
import net.roarsoftware.lastfm.Session;
import net.roarsoftware.lastfm.Track;
import net.roarsoftware.lastfm.User;
import net.roarsoftware.lastfm.scrobble.ResponseStatus;
import net.roarsoftware.lastfm.scrobble.Scrobbler;
import net.roarsoftware.lastfm.scrobble.SubmissionData;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.FileFilterUtils;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.farng.mp3.TagException;
import org.joda.time.Days;
import org.joda.time.Duration;
import org.joda.time.Instant;
import org.joda.time.Minutes;

import com.google.common.collect.Lists;

import eu.sweetlygeek.scrobbler.enumeration.Status;
import eu.sweetlygeek.scrobbler.exception.ScrobblerException;
import eu.sweetlygeek.scrobbler.graphical.Credentials;
import eu.sweetlygeek.scrobbler.model.MP3File;

public class PhoneScrobbler {

	private static final String ID = "phs";
	private static final String VERSION = "1.0";
	private static final String API_KEY = "7e505833348cddad03874cf166d85684";
	private static final String SECRET = "cd4972327f4cffabb18c7e2afba8faa5";

	private static final Logger LOGGER = Logger.getLogger(PhoneScrobbler.class);

	private Scrobbler scrobbler;
	private List<SubmissionData> datas;
	private Instant lastPlayed;
	private String user;
	private String password;
	private Map<File, Throwable> conversionErrors;

	public PhoneScrobbler(String user, String password) throws ScrobblerException
	{
		this.user = user;
		this.password = password;
		this.datas = new ArrayList<SubmissionData>();
		this.lastPlayed = getTimeOfLastScrobble();
		this.conversionErrors = new HashMap<File, Throwable>();
		initScrobbler();
	}

	private void initScrobbler() throws ScrobblerException {
		scrobbler = Scrobbler.newScrobbler(ID, VERSION, user);
		Session session = Authenticator.getMobileSession(user, password, API_KEY, SECRET);
		try {
			ResponseStatus response = scrobbler.handshake(session);
			handleResponse(response, "Error during handshake : ");
		} catch (IOException e) {
			throw new ScrobblerException("Error during handshake", e);
		}
	}

	private void handleResponse(ResponseStatus response, String errorMessage) throws ScrobblerException {
		if (!response.ok())
		{
			if (response.getStatus() == ResponseStatus.FAILED)
			{
				throw new ScrobblerException(errorMessage + response.getMessage());
			}
			else
			{
				throw new ScrobblerException(errorMessage + statusToString(response.getStatus()));
			}
		}
	}

	private String statusToString(int status)
	{
		return Status.values()[status].toString();
	}

	private Instant getTimeOfLastScrobble()
	{
		Collection<Track> last = User.getRecentTracks(user, 1, API_KEY);
		if (last.isEmpty())
		{
			return new Instant().minus(Days.days(1).toStandardDuration());
		}
		else
		{
			Track lastPlayed = last.iterator().next();
			return new Instant(lastPlayed.getPlayedWhen().getTime());
		}
	}

	@SuppressWarnings("unchecked")
	private void browseDirectories(File rootDir) throws ScrobblerException
	{
		Minutes available = new Duration(lastPlayed, new Instant()).toStandardSeconds().toStandardMinutes();
		int maxTracks = (available.getMinutes() / 5) - datas.size();

		Collection<File> fFiles = FileUtils.listFiles(rootDir, FileFilterUtils.suffixFileFilter(".mp3"), TrueFileFilter.INSTANCE);
		List<File> files = new ArrayList<File>(fFiles);
		Collections.sort(files);

		if (files.size() > maxTracks)
		{
			throw new ScrobblerException("Too much files : can only scrobble " + maxTracks);
		}

		Iterator<File> it = files.iterator();
		int i = 0;
		Instant current = lastPlayed;
		while (it.hasNext() && i < maxTracks)
		{
			File f = it.next();
			try {
				MP3File mp3 = new MP3File(f);
				int delay = Math.max(5, mp3.getLength() / 60);
				current = current.plus(Minutes.minutes(delay).toStandardDuration());
				SubmissionData data = mp3.toSubmissionData(current);
				datas.add(data);
			} catch (IOException e) {
				LOGGER.warn("Unable to parse mp3 file " + f.getName(), e);
				this.conversionErrors.put(f, e);
			} catch (TagException e) {
				LOGGER.warn("Unable to parse mp3 file " + f.getName(), e);
				this.conversionErrors.put(f, e);
			}
		}
	}

	private void realScrobble() throws ScrobblerException
	{
		if (datas != null && scrobbler != null)
		{
			try {
				List<List<SubmissionData>> datas = Lists.partition(this.datas, 50);
				for (List<SubmissionData> dataPart : datas)
				{
					ResponseStatus response = scrobbler.submit(dataPart);
					handleResponse(response, "Error while submitting : ");
				}
			} catch (IOException e) {
				throw new ScrobblerException("Error while submitting", e);
			}
		}
	}

	public void scrobble(File[] dirs) throws ScrobblerException
	{
		for (File dir : dirs)
		{
			browseDirectories(dir);
		}
		realScrobble();
	}

	public static void openFiles(String user, String password) throws ScrobblerException
	{
		JFileChooser fc = new JFileChooser();
		fc.setMultiSelectionEnabled(true);
		fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);

		int result = fc.showOpenDialog(null);
		if (result == JFileChooser.APPROVE_OPTION)
		{
			File[] paths = fc.getSelectedFiles();
			if (paths.length != 0)
			{
				PhoneScrobbler scrobbler = new PhoneScrobbler(user, password);
				scrobbler.scrobble(paths);
				if (!scrobbler.conversionErrors.isEmpty())
				{
					int response = JOptionPane.showConfirmDialog(null, "Some file could not be scrobbled. Would you like to see the errors ?");
					if (response == JOptionPane.OK_OPTION)
					{
						StringBuffer buf = new StringBuffer();
						for (Map.Entry<File, Throwable> entry : scrobbler.conversionErrors.entrySet())
						{
							try {
								StringWriter sw = new StringWriter();
								entry.getValue().printStackTrace(new PrintWriter(sw));
								buf.append(entry.getKey().getCanonicalPath() + " : " + sw.toString());
								buf.append("\n");
							} catch (IOException e) {
								// Nothing
							}
						}
						JOptionPane.showMessageDialog(null, buf.toString(), "Errors", JOptionPane.ERROR_MESSAGE);
					}
				}
			}
		}
	}

	public static void main(String[] args) {
		try {
			setLookAndFeel();
			BasicConfigurator.configure();
			Caller.getInstance().setDebugMode(true);
			Credentials.launch();
		} catch (ScrobblerException e) {
			displayError("Error while scrobbling", e);
			Credentials.destroy();
		}
	}
	
	private static void setLookAndFeel() throws ScrobblerException
	{
		String os = System.getProperty("os.name");
		String laf = UIManager.getCrossPlatformLookAndFeelClassName();
		if (os.toLowerCase().contains("windows"))
		{
			laf = UIManager.getSystemLookAndFeelClassName();
		}
		else
		{
			for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels())
			{
				if ("nimbus".equalsIgnoreCase(info.getName()))
				{
					laf = info.getClassName();
				}
			}
		}
		try {
			UIManager.setLookAndFeel(laf);
		} catch (ClassNotFoundException e) {
			throw new ScrobblerException("Error with LAF", e);
		} catch (InstantiationException e) {
			throw new ScrobblerException("Error with LAF", e);
		} catch (IllegalAccessException e) {
			throw new ScrobblerException("Error with LAF", e);
		} catch (UnsupportedLookAndFeelException e) {
			throw new ScrobblerException("Error with LAF", e);
		}
	}

	public static void displayError(String message, Throwable t)
	{
		LOGGER.error(message, t);
		JOptionPane.showMessageDialog(null, message + "\n" + t.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
	}
}
